/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file audio.hpp
 * @author TooOld2Rock'nRoll
 * @date 2024/07/11
 * @brief A complete audio API for the ArcadeFighter Library.
 * Thread safe singleton according to Game Programming Patterns, it could also have been implemented as a static class.<br>
 * Built with the help of Guy Somberg's book series "<i>Game Audio Programming, principles and practices</i>".<br>
 * SDL2 Mixer is used for the native API that actually handles all the low level calls.
 *
 * @todo Spatial audio is not working great. Change set position for real panning as suggested in SDL documentation when using just 2 channels.
 * @todo Implement post processing (what SDL call effects....).
 * @todo Implement groups of tracks to apply operations on those separated. User may want to partition the track list by game area, for instance, the game UI, player, environment, etc. This would create an obligation to enforce virtualized effects to comeback to the same track (group set??) it was playing before.
 * @todo Good candidate for first unit testing module.
 * @todo One day, go for a Audio Engine implementation (see todo file).
 *
 * @see: https://gameprogrammingpatterns.com/singleton.html
 * @see: https://wiki.libsdl.org/SDL2_mixer/CategoryAPI
 */
#ifndef _AUDIO_HPP_
#define _AUDIO_HPP_

/*---- Includes ----*/
#include <thread>
#include <vector>

#include <glm/glm.hpp>

#include "music_sources.hpp"
#include "effect_sources.hpp"
#include "properties.hpp"


/*---- Class Declaration ----*/
/**
 * @brief A complete Audio API that does a little more than a jukebox.
 *
 * This is technically part of the ArceadeFighter game engine, but it works as an independent module.<br>
 * The basic usage is straightforward, one must first open a connection to the underling audio device that will actually
 *   play the sounds and, upon success, load the desired resources and use the different operations to control the audio
 *   playback as desired.<br>
 *
 * To play a background music it would look something like this:<br>
 * \code
 *      Audio &instance = Audio::instance ();
 *      instance.open (); //it will use the default system device and avarage quality options.

 *      MusicFileSource *music_source = new MusicFileSource ("path to file");
 *      MusicFile background(music_source);
 *      instance.play (&background); //non-blocking call!
 *
 *      //Don't forget to delete the audio sources before exitting...
 *      //Don't forget to close the Audio when no longer needed!
 * \endcode
 *
 * We separate Sources and Containers so the actual data that is passed around can be loaded only once and is independent
 *   from the current playback state of such data, this is specially useful for Effects that will probably be reused for
 *   different characters and possibly played at the same time repeatedly.<br>
 * All calls to the API are non blocking, which means you can play and forget any audio and all the management of the
 *   playback life cycle will be done by the API in a background thread. That said, if you need a finer control of what
 *   is going on, you are encouraged to extend any of the Container classes.<br>
 *
 * There is both a limit of tracks available to playback Effects and the amount of sound sources a person
 *   can hear and still make sense of it all.<br>
 * The first problem is easily dealt with, be mindful of setting Effects as important or not and the API
 *   will do a best effort approach to make sure important sounds are always played (like the main character might
 *   blows) in detriment of not important ones (like one of many enemies dying cries).<br>
 * The second one tough is a little more challenging and requires the user discretion, the theory behind this
 *   however is beyond the scope of this documentation, be sure to read the sources noted at this file header.<br>
 * There are two ways to handle this without further research, try to avoid triggering the same effect in rapid
 *   succession and limit the number of tracks available to a reasonable amount. What is "rapid" and what is
 *   "reasonable" is up to you to decide.<br>
 *
 * @remarks The audio files supported by the API are defined in compilation time, SDL_Mixer depends on a list received
 *     upon initialization, this list is defined at ArcadeFighter's config.h file, but a fallback define is present at
 *     this API source file to keep it independent from the main library.
 */
class Audio
{
    public:
        /** @brief Holds information regarding the opened audio device in the system. */
        struct audio_device_st
        {
            std::string s_device_name; ///<The device name.
            unsigned u_frequency = 0; ///<The supported frequency.
            SDL_AudioFormat t_format = 0; ///<The expected format for data.
            unsigned u_channels = 0; ///<Number of channels to use for playback.
            unsigned u_chunksize = 0; ///<The size in bytes of read/write buffers.
        };//end audio_device_s


        /**
         * @brief Implements how to deal with a music audio source that is loaded from files.
         *
         * Compared to Effects, Music is a very simple class.<br>
         * You can have only one Music playing at any given time, trying to play a new one will just interrupt the
         *   current playing one, so it is better suited for soundtracks and background ambiance music.
         *
         * @remarks A Music can´t be live processed, there is no underling mechanism that permits that, but you can add
         *     effects in post processing over the entire audio stream just before actually playing it.
         */
        class MusicFile : public AudioContainer<UndecodedFileSource>
        {
            friend Audio;

            private:
                //makes little sense to repeat a sound track for a few times
                using AudioContainer::repeat;
                using AudioContainer::remainingRepeatCount;

            protected:
                virtual bool setState (sound_state_e new_state) override;
                virtual void update (double delta_t) override;


            public:
                /** @brief Default constructor. */
                MusicFile () : AudioContainer () {  }
                /** @brief Constructor with audio source. */
                MusicFile (UndecodedFileSource *data) : AudioContainer(data) { }
        };//END MusicFile

        /**
         * @brief Implements how to deal with a music audio source streamed from a buffer.
         *
         * This is the option if you intend to auto generate audio or don't want a large audio file using too much RAM.<br>
         * It will still accept to playback only one Music container at a time, so this is probably the only option if
         *    you require to play several concurrently.
         */
        class MusicStreaming : public AudioContainer<MusicStreamSource>
        {
            friend Audio;

            private:
                //SDL Mixer offers no capability that would make sense of this, user is in control of it all!
                using AudioContainer::setFadeIn;
                using AudioContainer::setFadeOut;
                using AudioContainer::loop;
                using AudioContainer::isOnLoop;
                using AudioContainer::repeat;
                using AudioContainer::remainingRepeatCount;

            protected:
                virtual bool setState (sound_state_e new_state) override;
                virtual void update (double delta_t) override;


            public:
                /** @brief Default constructor. */
                MusicStreaming () : AudioContainer () {  }
                /** @brief Constructor with audio source. */
                MusicStreaming (MusicStreamSource *data) : AudioContainer(data) { }
        };//END MusicStreaming


        /**
         * @brief Implements how to deal with an effect audio source that is loaded from files.
         *
         * Effects have a more robust life cycle than Music, there can be many playing at the same time, they (mostly)
         *    spawn to existence and disappear in instants.<br>
         * They work better as, wait for it!, sound effects around the game.<br>
         *
         * You can´t stream effects from buffers (well, you could hijack the MusicStreamSource :D), but you can apply
         *   filters to them as they play.<br>
         * We also offer a rudimentary spatial audio implementation, setting the position of the microfone (in world
         *   coordinates) and the position of the Effects will trigger this feature.
         *
         * @remarks Be mindful of how you set effects as important or not as this will have great effect on how the API
         *    correctly manages your expectations of which sounds should be dropped or overridden when the Audio
         *    resources become crowded. All Effects are set as <b>not</b> important by default.
         */
        class Effect : public AudioContainer<DecodedFileSource>
        {
            friend Audio;

            protected:
                bool b_important = false; ///<If the Effect has priority in the playback tracks.

                Position v3_position; ///<The effect world position (for rudimentary 3D audio)

                virtual bool setState (sound_state_e new_state) override;
                virtual void update (double delta_t) override;


            public:
                /** @brief Default constructor. */
                Effect () : AudioContainer () {  }
                /** @brief Constructor with audio source. */
                Effect (DecodedFileSource *data) : AudioContainer(data) { }

                /** @brief If the sound may or may not be replaced wile in playback.
                 * Sound tracks are limited, by setting an effect as not important, it signals the API that it can be
                 *    halted and replaced by a important one automatically when out of tracks to use.
                 */
                void setImportance (bool important) { this->b_important = important; }
                /** @brief Gets this Effect importance status. */
                bool isImportant () const { return this->b_important; }

                /** @brief Gets this Effect world position property. */
                Position & getPosition () { return this->v3_position; }
        };//END Effect


    private:
        std::atomic<bool> b_is_open; //if the audio device is open to play sounds.
        bool b_mute = false; //if all sounds must be muted
        audio_device_st st_audio_device_info; //important information about the open device.

        std::thread *p_service_thread = nullptr; //A thread to perform background tasks.
        time_keep_s _thread_delta_t; //Keeps track of loop time on service thread.

        float f_master_volume = 1.f; //an overall volume to regulate all the others.
        unsigned short us_musics_volume = 100; //overall volume for music
        unsigned short us_effects_volume = 100; //overall volume for effects

        float f_min_distance = 0.f;
        float f_max_distance = 255.f;
        Position v3_microphone_position; //for rudimentary 3D audio, the "listener" world position.

        MusicFile *p_music_playing = nullptr; //the Music currently playing.
        MusicStreaming *p_music_streaming = nullptr; //the Music currently playing.

        unsigned u_track_count = 0; //the number of tracks available for Effects to play in parallel.
        Audio::Effect **a_tracks = nullptr; //keeps the list of tracks to play effects (short sounds)


        //hide the constructor to make the singleton
        Audio ();
        void _serviceThread_cb ();
        static void _music_finished_cb ();
        static void _track_finished_cb (AudioTrackID_t track);

        void update (double delta_t);

    protected:


    public:
        static Audio& instance ();
        ~Audio ();

        //delete copy and move constructors and assign operators to make singleton safe
        Audio (Audio const&) = delete;            ///<Copy construct
        Audio (Audio &&) = delete;                ///<Move construct
        Audio& operator= (Audio const&) = delete; ///<Copy assign
        Audio& operator= (Audio &&) = delete;     ///<Move assign

        static void getAudioDevicesList (std::vector<std::string> &device_list);
        bool open (unsigned frequency = MIX_DEFAULT_FREQUENCY, unsigned short format = MIX_DEFAULT_FORMAT,
                   unsigned channels = MIX_DEFAULT_CHANNELS, unsigned chunksize = 2048,
                   const char *device = nullptr);
        /** @brief Pointer to the active audio device info if track is open, null otherwise. */
        const Audio::audio_device_st * getAudioDeviceInfo () const { return (this->b_is_open ? &this->st_audio_device_info : nullptr); }
        /** @brief <b>True</b> if the Audio Device is open, <b>false</b> otherwise. */
        bool isOpen () const { return this->b_is_open; }
        void close ();

        /*** Global Methods ***/
        void mute (bool m);
        /** @brief <b>True</b> if all audio is muted, <b>false</b> otherwise. */
        bool isMuted () const { return this->b_mute; }
        void setMasterVol (unsigned short volume);
        /** @brief Returns the current Master Volume Level (0-100). */
        unsigned short getMasterVol () const { return this->f_master_volume * 100; }
        void pause ();
        void resume ();
        void halt ();
        /** @brief Checks if the API is currently playing anything. */
        bool isPlaying () const { return (this->isPlayingMusic() || this->isPlayingEffect()); }

        void setWorldDistances (float min, float max);
        /** @brief Gets the microfone (listener) world position property. */
        Position & getListenerPosition () { return this->v3_microphone_position; }

        /*** Music Methods ***/
        void setMusicsVol (unsigned short volume);
        /** @brief Returns the current base volume Level for Music (0-100). */
        unsigned short getMusicsVol () const { return this->us_musics_volume * 100; }

        void play (MusicFile *mu);
        void play (MusicStreaming *mu);
        void pauseMusic ();
        void resumeMusic ();
        void stopMusic (double fade_out = 0);
        /** @brief Checks if any Music is playing (if sound should be heard at the opened device, unless paused). */
        bool isPlayingMusic () const { return (Mix_PlayingMusic() || Mix_PausedMusic()); }

        /*** Effects Methods ***/
        void setTrackCount (unsigned num_tracks);
        /** @brief Returns the max number of tracks used to playback Effects simultaneously. */
        unsigned getTrackCount () const { return this->u_track_count; }

        void setSoundEffectsVol (unsigned short volume);
        /** @brief Returns the current base volume Level for Effects (0-100). */
        unsigned short getSoundEffectsVol () const { return this->us_effects_volume; }

        AudioTrackID_t play (AudioTrackID_t track_id, Effect *ef);
        AudioTrackID_t play (Effect *ef);
        void pauseAllEffects ();
        void pauseEffect (AudioTrackID_t track);
        void pauseEffect (Effect &ef);
        void resumeAllEffects ();
        void resumeEffect (AudioTrackID_t track);
        void resumeEffect (Effect &ef);
        void stopAllEffects (double fade_out = 0);
        void stopEffect (AudioTrackID_t track, double fade_out = 0);
        void stopEffect (Effect &ef, double fade_out = 0);
        /** @brief Checks if any Effect is playing (if sound should be heard at the opened device). */
        bool isPlayingEffect () const { return (Mix_Playing(-1) || Mix_Paused(-1)); }
        /** @brief Checks if Effect is playing (if sound should be heard at the opened device). */
        bool isPlayingEffect (AudioTrackID_t track) const { return (Mix_Playing(track) || Mix_Paused(track)); }
};//END Audio

#endif //_AUDIO_HPP_

